feat: source-priority tiebreaker in affiliation timeline builder (CM-1106)#4055
feat: source-priority tiebreaker in affiliation timeline builder (CM-1106)#4055
Conversation
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds source-aware tie-breaking to the affiliation timeline builder’s primary work-experience selection so that overlapping dated work experiences prefer higher-trust sources (ui > email-domain > enrichment-* > others) before existing heuristics.
Changes:
- Introduces
getMemberOrganizationSourceRankin@crowd/commonto rank member-organization sources. - Applies source-tier tie-breaking in
selectPrimaryWorkExperiencewhen multiple dated rows overlap. - Includes
mo."source"in the memberOrganizations timeline query so source-based selection is possible.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| services/libs/data-access-layer/src/member-organization-affiliation/index.ts | Adds source-tier tie-break step and selects source from DB for timeline building. |
| services/libs/common/src/member.ts | Adds a shared helper for ranking member-organization sources. |
Comments suppressed due to low confidence (1)
services/libs/data-access-layer/src/member-organization-affiliation/index.ts:99
memberOrgsOnlyis filtered by'segmentId' in row, butsegmentIdis what distinguishes manual affiliations (IManualAffiliationData). In this branch manual affiliations have already been returned early, so this filter will always be empty and the memberCount tiebreaker will never run. Filter for actual member-organization rows instead (e.g., exclude items withsegmentId, or use a type guard onMemberOrganizationWithOverrides).
// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
) as MemberOrganizationWithOverrides[]
if (memberOrgsOnly.length >= 2) {
const sortedByMembers = memberOrgsOnly.sort((a, b) => b.memberCount - a.memberCount)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 25bcf9a. Configure here.
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
services/libs/data-access-layer/src/member-organization-affiliation/index.ts:97
memberOrgsOnlyis intended to exclude manual affiliations (which don’t havememberCount), but the current predicate ('segmentId' in row && !!row.segmentId) actually selects manual affiliations. SinceMemberOrganizationWithOverrides(IMemberOrganization) doesn’t havesegmentId, this filter will usually return an empty array and the member-count tiebreaker will never run. Consider filtering by the absence ofsegmentId(or using a proper type guard) so member orgs are compared bymemberCountas intended.
// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
) as MemberOrganizationWithOverrides[]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
services/libs/data-access-layer/src/member-organization-affiliation/index.ts:98
- The
memberOrgsOnlyfilter is inverted: it selects rows that havesegmentId(manual affiliations), but this branch only runs when there are no manual affiliations, somemberOrgsOnlywill always be empty and the memberCount tiebreaker never executes. Filter for member-organization rows instead (e.g.,!('segmentId' in row)or'memberCount' in row) so memberCount comparison works as intended.
// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
) as MemberOrganizationWithOverrides[]
if (memberOrgsOnly.length >= 2) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Adds source-aware tiebreaking to
selectPrimaryWorkExperiencein both the affiliation timeline builder and the public API path.When multiple dated
memberOrganizationsrows cover the same day, the winner is now decided by source tier before falling through to the existing member-count and date-range heuristics:ui → email-domain → enrichment-* → anything else
Source priority is only applied to dated rows, so undated rows remain last-resort regardless of source.
Note
Medium Risk
Changes the tie-breaking logic that decides which organization “wins” during overlapping dated affiliations, which can alter timeline outputs in user-visible and downstream data processing paths. Scope is limited to ranking heuristics and query projection of
source, with added tests to pin behavior.Overview
Adds a new
getMemberOrganizationSourceRankhelper and wires it intoselectPrimaryWorkExperiencein both the public affiliations resolver and the internal member affiliation timeline builder so that, when multiple dated rows overlap, source tier (ui>email-domain>enrichment-*> other) is used before falling back to member count and longest date range.Updates the affiliations queries/types to include
memberOrganizations.sourcein the resolution inputs, and expands unit tests to cover the new source-priority outcomes and ensure undated rows remain unaffected by this ranking.Reviewed by Cursor Bugbot for commit c61a950. Bugbot is set up for automated code reviews on this repo. Configure here.